//The MIT License
//
//Copyright (c) 2009 nodchip
//
//Permission is hereby granted, free of charge, to any person obtaining a copy
//of this software and associated documentation files (the "Software"), to deal
//in the Software without restriction, including without limitation the rights
//to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//copies of the Software, and to permit persons to whom the Software is
//furnished to do so, subject to the following conditions:
//
//The above copyright notice and this permission notice shall be included in
//all copies or substantial portions of the Software.
//
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//THE SOFTWARE.
package tv.dyndns.kishibe.server;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.Map.Entry;

import tv.dyndns.kishibe.client.Constant;
import tv.dyndns.kishibe.client.packet.PacketProblemData;
import tv.dyndns.kishibe.client.packet.PacketProblemDataMinimum;

public class ProblemManager {
	public static class PacketProblemDataMinimums extends ArrayList<PacketProblemDataMinimum> {
		private static final long serialVersionUID = 3261998012051054182L;
	}

	private static final ProblemManager INSTANCE = new ProblemManager();
	private volatile PacketProblemDataMinimums[][] problemTables = new PacketProblemDataMinimums[Constant.NUMBER_OF_GENRE][Constant.NUMBER_OF_TYPE_WITH_RANDOM];
	private volatile Map<String, PacketProblemDataMinimums> problemTablesForThemeMode = new HashMap<String, PacketProblemDataMinimums>();
	private final Random random = new Random();
	private static final int MAX_FIND_LOOP = 100;
	private static final double NEW_RATIO = 0.5;
	// 各ジャンル・出題形式のカウント
	private final int[][] tableProblemCount = new int[Constant.NUMBER_OF_GENRE][Constant.NUMBER_OF_TYPE_WITH_RANDOM];
	private final int[][] tableProblemRatio = new int[Constant.NUMBER_OF_GENRE][11];

	public static ProblemManager getInstance() {
		return INSTANCE;
	}

	private ProblemManager() {
		for (int genre = 0; genre < Constant.NUMBER_OF_GENRE; ++genre) {
			for (int type = 0; type < Constant.NUMBER_OF_TYPE_WITH_RANDOM; ++type) {
				problemTables[genre][type] = new PacketProblemDataMinimums();
			}
		}

		loadMinimumProblemData();
		makeReport();
		refreshProblemTablesForThemeMode();

		ThreadPool.getInstance().addHourTask(new Runnable() {
			public void run() {
				makeReport();
				refreshProblemTablesForThemeMode();
			}
		});
	}

	private void loadMinimumProblemData() {
		final List<PacketProblemDataMinimum> datas = CachedDatabase.getInstance().getMinimumProblemDatas();
		for (PacketProblemDataMinimum data : datas) {
			final int genre = data.genre;
			final int type = data.type;
			final int randomFlag = data.randomFlag;

			problemTables[genre][type].add(data);
			problemTables[genre][0].add(data);
			problemTables[0][type].add(data);
			problemTables[0][0].add(data);
			problemTables[genre][Constant.TYPE_RANDOM_FLAG_OFFSET + randomFlag].add(data);
			problemTables[0][Constant.TYPE_RANDOM_FLAG_OFFSET + randomFlag].add(data);
		}
	}

	public synchronized PacketProblemDataMinimum selectProblemForNormalMode(int genre, int type, int classLevel, int difficultSelect, Set<Integer> selectedProblemIds, boolean first, boolean newProblem, boolean tegaki) throws Exception {
		// ジャンルの決定
		int genreBitMask = genre;
		if (genreBitMask <= 1) {
			// ジャンルが選択されていない場合は全ジャンルから出題
			// ただしノンジャンルフラグは除く
			genreBitMask = (1 << Constant.NUMBER_OF_GENRE) - 2;
		}

		// 出題形式の決定
		int typeBitMask = type;
		if (typeBitMask <= 1) {
			// 出題形式が選択されていない場合は全出題形式から出題
			// ただし全フラグは除く
			typeBitMask = (1 << Constant.NUMBER_OF_TYPE_WITH_RANDOM) - 2;
		}

		// 手書きクイズは出題見合わせ
		if (!tegaki) {
			if ((typeBitMask & (1 << Constant.TYPE_TEGAKI)) != 0) {
				typeBitMask -= (1 << Constant.TYPE_TEGAKI);
			}
			if (typeBitMask == 0) {
				typeBitMask = (1 << Constant.NUMBER_OF_TYPE_WITH_RANDOM) - 2 - (1 << Constant.TYPE_TEGAKI);
			}
		}

		// 難易度調整
		switch (difficultSelect) {
		case Constant.DIFFICULT_SELECT_NORMAL:
			classLevel = Constant.CLASS_LEVEL_NORMAL;
			break;
		case Constant.DIFFICULT_SELECT_DIFFICULT:
			classLevel = Constant.CLASS_LEVEL_DIFFICULT;
			break;
		case Constant.DIFFICULT_SELECT_LITTLE_DIFFICULT:
			classLevel = Constant.CLASS_LEVEL_LITTLE_DIFFICULT;
			break;
		case Constant.DIFFICULT_SELECT_LITTLE_EASY:
			classLevel = Constant.CLASS_LEVEL_LITTLE_EASY;
			break;
		case Constant.DIFFICULT_SELECT_EASY:
			classLevel = Constant.CLASS_LEVEL_EASY;
			break;

		}

		// 問題の選択
		PacketProblemDataMinimum data = null;
		for (int findLoop = 0; findLoop < MAX_FIND_LOOP && data == null; ++findLoop) {
			// 出題するジャンルと出題形式を決定する
			int selectedGenre;
			int selectedType;
			int genreAndTypeFindLoop = MAX_FIND_LOOP;
			do {
				selectedGenre = selectFromBit(genreBitMask);
				selectedType = selectFromBit(typeBitMask);
			} while (problemTables[selectedGenre][selectedType].isEmpty() && --genreAndTypeFindLoop > 0);

			// 問題が登録されていなかった場合の処理
			if (problemTables[selectedGenre][selectedType].isEmpty() || genreAndTypeFindLoop == 0) {
				selectedGenre = 0;
				selectedType = 0;
			}

			final List<PacketProblemDataMinimum> problems = problemTables[selectedGenre][selectedType];
			data = selectProblemFromList(problems, selectedProblemIds, classLevel, newProblem, true, tegaki);

			// ランダムの場合はtegakiフラグにかかわらず手書きクイズを出さない
			if (data != null && typeBitMask >= (1 << Constant.TYPE_RANDOM_FLAG_OFFSET) && data.type == Constant.TYPE_TEGAKI) {
				data = null;
			}
		}

		if (data == null) {
			// 問題が選択されなかった場合は全ジャンルから選択しなおす

			if (difficultSelect != Constant.DIFFICULT_SELECT_NORMAL) {
				// 難易度を広げて再度選択
				data = selectProblemForNormalMode(genre, type, classLevel, Constant.DIFFICULT_SELECT_NORMAL, selectedProblemIds, true, newProblem, tegaki);
			} else if (type != 0) {
				// 出題形式を変更して再度選択
				data = selectProblemForNormalMode(genre, 0, classLevel, difficultSelect, selectedProblemIds, true, newProblem, tegaki);
			} else if (genre != 0) {
				// ジャンルを変えて再度選択
				data = selectProblemForNormalMode(0, 0, classLevel, difficultSelect, selectedProblemIds, true, newProblem, tegaki);
			} else if (first) {
				// もう一度だけ選択
				data = selectProblemForNormalMode(0, 0, classLevel, difficultSelect, selectedProblemIds, false, newProblem, tegaki);
			} else {
				throw new Exception("問題が見つかりませんでした");
			}
		}

		selectedProblemIds.add(data.problemId);

		return data;
	}

	private final int selectFromBit(int bits) {
		if (bits == 0) {
			return 0;
		}

		final List<Integer> l = new ArrayList<Integer>();
		for (int shift = 0; shift < 32; ++shift) {
			if ((bits & (1 << shift)) != 0) {
				l.add(shift);
			}
		}

		if (l.isEmpty()) {
			return 0;
		}

		return l.get(random.nextInt(l.size()));
	}

	private PacketProblemDataMinimum selectProblemFromList(List<PacketProblemDataMinimum> problems, Set<Integer> selectedProblemIds, int classLevel, boolean newProblem, boolean biasForNewProblems, boolean tegaki) {
		if (problems.isEmpty()) {
			return null;
		}

		// なるべく新しい問題が出るようにする
		final int index;
		if (biasForNewProblems) {
			final double zeroOne = Math.pow(random.nextDouble(), NEW_RATIO);
			index = (int) (zeroOne * problems.size());
		} else {
			index = random.nextInt(problems.size());
		}
		PacketProblemDataMinimum problemData = problems.get(index);

		// 同じ問題が出題されないようにする
		if (selectedProblemIds.contains(problemData.problemId)) {
			return null;
		}

		// 新問・集計中問題の取り扱い
		if (problemData.isNew()) {
			if (classLevel < Constant.NUMBER_OF_CLASSES / 10 || classLevel == Constant.CLASS_LEVEL_EASY || !newProblem) {
				// 初心者クラスでは新問は出題しない
				return null;
			}
		} else {
			// 通常の問題の取り扱い
			final int ratio = problemData.getCorrectRatio();
			final int lower = Constant.getDifficultyLower(classLevel);
			final int upper = Constant.getDifficultyUpper(classLevel);
			if (ratio < lower || upper < ratio) {
				return null;
			}
		}

		if (!tegaki && problemData.type == Constant.TYPE_TEGAKI) {
			return null;
		}

		return problemData;
	}

	synchronized public int addProblemData(PacketProblemData data) {
		CachedDatabase.getInstance().addProblemData(data);

		final PacketProblemDataMinimum dataMinimum = new PacketProblemDataMinimum();
		final int genre = dataMinimum.genre = data.genre;
		final int type = dataMinimum.type = data.type;
		dataMinimum.good = data.good;
		dataMinimum.bad = data.bad;
		dataMinimum.randomFlag = data.randomFlag;
		dataMinimum.problemId = data.problemId;

		final int randomFlag = Constant.TYPE_RANDOM_FLAG_OFFSET + dataMinimum.randomFlag;
		problemTables[genre][type].add(dataMinimum);
		problemTables[0][type].add(dataMinimum);
		problemTables[genre][0].add(dataMinimum);
		problemTables[0][0].add(dataMinimum);
		problemTables[genre][randomFlag].add(dataMinimum);
		problemTables[0][randomFlag].add(dataMinimum);

		return dataMinimum.problemId;
	}

	synchronized public int getNumberOfProblem() {
		return problemTables[0][0].size();
	}

	synchronized public void updateProblemData(final PacketProblemData data) {
		final PacketProblemDataMinimum dataMinimum = new PacketProblemDataMinimum();
		final int id = dataMinimum.problemId = data.problemId;
		final int genre = dataMinimum.genre = data.genre;
		final int type = dataMinimum.type = data.type;
		dataMinimum.good = data.good;
		dataMinimum.bad = data.bad;
		dataMinimum.randomFlag = data.randomFlag;

		for (List<PacketProblemDataMinimum>[] lists : problemTables) {
			for (List<PacketProblemDataMinimum> list : lists) {
				for (int i = list.size() - 1; i >= 0; --i) {
					if (list.get(i).problemId == id) {
						list.remove(i);
					}
				}
			}
		}

		final int randomFlag = Constant.TYPE_RANDOM_FLAG_OFFSET + dataMinimum.randomFlag;
		problemTables[genre][type].add(dataMinimum);
		problemTables[0][type].add(dataMinimum);
		problemTables[genre][0].add(dataMinimum);
		problemTables[0][0].add(dataMinimum);
		problemTables[genre][randomFlag].add(dataMinimum);
		problemTables[0][randomFlag].add(dataMinimum);

		ThreadPool.getInstance().execute(new Runnable() {
			public void run() {
				CachedDatabase.getInstance().updateProblemData(data);
			}
		});
	}

	synchronized public void updateMinimumProblemData(final PacketProblemDataMinimum data) {
		final int id = data.problemId;
		final int genre = data.genre;
		final int type = data.type;

		final List<List<PacketProblemDataMinimum>> datasList = new ArrayList<List<PacketProblemDataMinimum>>();
		datasList.add(problemTables[genre][type]);
		datasList.add(problemTables[genre][0]);
		datasList.add(problemTables[0][type]);
		datasList.add(problemTables[0][0]);

		for (List<PacketProblemDataMinimum> datas : datasList) {
			for (int i = 0; i < datas.size(); ++i) {
				if (datas.get(i).problemId == id) {
					datas.set(i, data);
				}
			}
		}

		ThreadPool.getInstance().execute(new Runnable() {
			public void run() {
				CachedDatabase.getInstance().updateMinimumProblemData(data);
			}
		});
	}

	synchronized private void makeReport() {
		for (int genre = 0; genre < Constant.NUMBER_OF_GENRE; ++genre) {
			for (int type = 0; type < Constant.NUMBER_OF_TYPE_WITH_RANDOM; ++type) {
				tableProblemCount[genre][type] = 0;
			}
		}
		for (int genre = 0; genre < Constant.NUMBER_OF_GENRE; ++genre) {
			for (int ratio = 0; ratio < 11; ++ratio) {
				tableProblemRatio[genre][ratio] = 0;
			}
		}

		for (PacketProblemDataMinimum problem : problemTables[0][0]) {
			++tableProblemCount[0][0];
			++tableProblemCount[problem.genre][0];
			++tableProblemCount[0][problem.type];
			++tableProblemCount[problem.genre][problem.type];
			++tableProblemCount[0][Constant.TYPE_RANDOM_FLAG_OFFSET + problem.randomFlag];
			++tableProblemCount[problem.genre][Constant.TYPE_RANDOM_FLAG_OFFSET + problem.randomFlag];

			int index;
			int ratio = problem.getCorrectRatio();
			if (ratio == -1) {
				index = Constant.REPORT_NOT_YET;
			} else {
				index = ratio / 10;
				index = Math.min(9, index);
			}
			++tableProblemRatio[0][index];
			++tableProblemRatio[problem.genre][index];
		}
	}

	synchronized public int[][] getTableProblemCount() {
		return tableProblemCount;
	}

	synchronized public int[][] getTableProblemRatio() {
		return tableProblemRatio;
	}

	public int getProblemCount() {
		return problemTables[0][0].size();
	}

	synchronized private void refreshProblemTablesForThemeMode() {
		final Map<String, List<String>> themeModeQuery = CachedDatabase.getInstance().getThemeModeQuery();

		final Map<String, PacketProblemDataMinimums> problemTables = new HashMap<String, PacketProblemDataMinimums>();
		for (Entry<String, List<String>> entry : themeModeQuery.entrySet()) {
			final String theme = entry.getKey();
			final List<String> queries = entry.getValue();
			problemTables.put(theme, CachedDatabase.getInstance().getMinimumProblemDatasForThemeMode(queries.toArray(new String[0])));
		}

		problemTablesForThemeMode = problemTables;
	}

	synchronized public PacketProblemDataMinimum selectProblemForThemeMode(String theme, int difficultSelect, int classLevel, Set<Integer> selectedProblemIds) throws Exception {
		// 難易度調整
		switch (difficultSelect) {
		case Constant.DIFFICULT_SELECT_DIFFICULT:
			classLevel = Constant.CLASS_LEVEL_DIFFICULT;
			break;
		case Constant.DIFFICULT_SELECT_LITTLE_DIFFICULT:
			classLevel = Constant.CLASS_LEVEL_LITTLE_DIFFICULT;
			break;
		case Constant.DIFFICULT_SELECT_LITTLE_EASY:
			classLevel = Constant.CLASS_LEVEL_LITTLE_EASY;
			break;
		case Constant.DIFFICULT_SELECT_EASY:
			classLevel = Constant.CLASS_LEVEL_EASY;
			break;
		case Constant.DIFFICULT_SELECT_NORMAL:
			classLevel = Constant.CLASS_LEVEL_NORMAL;
			break;
		}

		// 問題の選択
		PacketProblemDataMinimum data = null;
		final List<PacketProblemDataMinimum> problems = problemTablesForThemeMode.get(theme);
		for (int findLoop = 0; findLoop < MAX_FIND_LOOP && data == null; ++findLoop) {
			data = selectProblemFromList(problems, selectedProblemIds, classLevel, false, false, false);
		}

		if (data == null) {
			// 問題が選択されなかった場合は全ジャンルから選択しなおす

			if (difficultSelect != Constant.DIFFICULT_SELECT_NORMAL) {
				// 難易度を広げて再度選択
				data = selectProblemForThemeMode(theme, Constant.DIFFICULT_SELECT_NORMAL, classLevel, selectedProblemIds);
			} else {
				throw new Exception("問題が見つかりませんでした");
			}
		}

		selectedProblemIds.add(data.problemId);

		return data;
	}

	public String[] getThemeModeThemes() {
		return problemTablesForThemeMode.keySet().toArray(new String[0]);
	}
}

// CREATE TABLE IF NOT EXISTS problem ( ID INTEGER NOT NULL PRIMARY KEY
// AUTO_INCREMENT, GENRE INTEGER NOT NULL, TYPE INTEGER NOT NULL, SENTENCE TEXT
// NOT NULL, ANSWER0 TEXT NOT NULL, ANSWER1 TEXT NOT NULL, ANSWER2 TEXT NOT
// NULL, ANSWER3 TEXT NOT NULL, CHOICE0 TEXT NOT NULL, CHOICE1 TEXT NOT NULL,
// CHOICE2 TEXT NOT NULL, CHOICE3 TEXT NOT NULL, GOOD INTEGER NOT NULL, BAD
// INTEGER NOT NULL, CREATER TEXT NOT NULL, NOTE TEXT NOT NULL DEFAULT "",
// FULLTEXT (SENTENCE, ANSWER0,ANSWER1, ANSWER2, ANSWER3, CHOICE0, CHOICE1,
// CHOICE2, CHOICE3, CREATER, NOTE), FULLTEXT(CREATER) )CHARSET utf8 TYPE =
// MYISAM;
